进阶目标
前几节我们通过 TypeOrmModule.forRoot() 的 name 属性实现了静态多数据库配置。但这种方式需要在编码时就确定有哪些数据库连接,不够灵活。
本节演示一种动态连接方案:利用 useFactory 的特性,在每次请求时动态返回不同的数据库配置,实现真正的运行时多数据库切换。
核心思路
NestJS 的 useFactory 在每次请求时都会重新执行(当 Provider 是 Request 作用域时)。我们可以在 Factory 函数中注入一个自定义 Service,该 Service 根据请求中的租户标识返回不同的数据库端口/连接信息。
请求 → AppService.getDBPort(tenantId) → useFactory → TypeORM DataSource
text
实现步骤
1. 创建 AppService 管理数据库连接信息
// app.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class AppService {
constructor(
@Inject('REQUEST')
private request: Request,
) {}
/**
* 根据请求中的租户标识返回对应的数据库端口
*/
getDBPort(): number {
const tenantId = this.request.headers['x-tenant-id'] as string;
if (tenantId === 'mysql1') {
return 3307;
}
return 3306;
}
}
typescript
2. 在 TypeOrmModule.forRootAsync 中注入 AppService
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService, AppService],
useFactory: (configService: ConfigService, appService: AppService) => {
// 每次请求都会重新执行此函数
const port = appService.getDBPort();
return {
type: 'mysql',
host: configService.get('DB_HOST'),
port, // 动态端口
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
entities: [User],
synchronize: true,
};
},
}),
],
providers: [AppService],
})
export class AppModule {}
typescript
3. Controller 中使用
此时只需注入一个 Repository,不再需要 name 属性和多个注入:
// app.controller.ts
@Controller('api/v1')
export class AppController {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
@Get('hello')
async getData() {
const data = await this.userRepository.find();
return data;
}
}
typescript
useFactory 的执行时机验证
通过 console.log 打印验证 useFactory 是否在每次请求时都重新执行:
useFactory: (configService: ConfigService, appService: AppService) => {
const port = appService.getDBPort();
console.log('param appService.getDBPort():', port);
// ...
},
typescript
测试结果:
# 不带 x-tenant-id Header
curl http://localhost:3000/api/v1/hello
# 控制台输出:param appService.getDBPort(): 3306
# 带 x-tenant-id: mysql1 Header
curl -H "x-tenant-id: mysql1" http://localhost:3000/api/v1/hello
# 控制台输出:param appService.getDBPort(): 3307
bash
确认 useFactory 在每次请求时都会执行,实现了真正的动态连接。
动态方案 vs 静态方案对比
| 维度 | 静态方案(name 属性) | 动态方案(useFactory) |
|---|---|---|
| 数据库数量 | 编码时确定 | 运行时动态 |
| Repository 注入 | 需要多个 @InjectRepository | 只需一个 |
| 配置复杂度 | 较低 | 较高 |
| 适用场景 | 数据库数量固定且已知 | SaaS 多租户,租户数量不确定 |
| 性能 | 连接池预热,性能好 | 每次请求可能创建新连接 |
架构图
HTTP Request
↓
Controller(注入单一 Repository)
↓
TypeOrmModule.forRootAsync
↓ useFactory 每次请求执行
↓
AppService.getDBPort()
↓ 读取 request.headers['x-tenant-id']
↓
┌───────────┬───────────┐
│ tenant=null│ tenant=mysql1│
│ port=3306 │ port=3307 │
└─────┬─────┴─────┬─────┘
↓ ↓
MySQL A MySQL B
text
注意事项
- 性能考虑:动态方案每次请求都可能创建新的数据库连接。在生产环境中应配合连接池使用,避免频繁创建/销毁连接。
- 错误处理:当租户标识对应的数据库不存在时,需要优雅降级到默认数据库,而非抛出连接错误。
- 开发调试:可以在
AppService中添加日志,便于追踪每次请求使用了哪个数据库配置。 - 安全考虑:租户标识应从 JWT Token 中获取而非直接信任客户端 Header,防止租户越权访问。
小结
- 通过
useFactory+ 自定义 Service 实现运行时动态数据库选择 useFactory在 Request 作用域下每次请求都会重新执行- Controller 层只需注入一个 Repository,数据库选择逻辑完全透明
- 生产环境需注意连接池管理和安全性
- 此方案适合 SaaS 多租户场景,租户数量不固定
↑